﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using AbstraX.ServerInterfaces;
using AbstraX.Bindings;
using CodeGenerationPipeline.Contracts;
using System.Xml.Schema;
using System.Reflection;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Web;
using AbstraX.Contracts;
using ImpromptuInterface;
using ImpromptuInterface.Dynamic;
using Microsoft.Practices.Unity;
using AbstraX.TypeMappings;
using DataBindings;
using AbstraX;
using Metaspec;
using System.Collections;
using AbstraX.BuildEvents;
using Utils;
using AbstraX.Contracts.CodeParsers;

namespace CodeGenerationPipeline
{
    public class PipelineService : IPipelineService
    {
        private Dictionary<string, IElementBuild> elementBuilds;
        private XmlSchema codeGeneratorInterfaceSchema;
        private IDomainHostApplication host;
        private IGenerator currentGenerator;
        private IPipelineStep currentStep;
        private Queue<BuildInfo> buildInfoQueue;
        public IEventsService EventsService { get; set; }
        public event OnOutputHandler OnOutput;

        private class ProcessWatchInfo
        {
            public IGenerator Generator { get; set; }
            public Process Process { get; set; }
            public ManualResetEvent ProcessExitEvent { get; set; }
        }

        public PipelineService(IDomainHostApplication host)
        {
            this.host = host;
            elementBuilds = new Dictionary<string, IElementBuild>();
            buildInfoQueue = new Queue<BuildInfo>();

            RegisterSteps();
        }

        private void RegisterSteps()
        {
            var path = (string)null;

            if (HttpContext.Current != null)
            {
                path = new DirectoryInfo(HttpContext.Current.Request.PhysicalApplicationPath).Parent.FullName;
            }
            else
            {
                path = "%SFSOLUTIONPATH%".Expand();
            }

            var _null = (object)null;

            // todo - come from database

            var step = new
            {
                Name = "CsParserStep",
                StepAssemblyFile = Path.Combine(path, string.Format(@"CodeGenerationPipeline\PipelineGenerators\{0}\bin\Debug\{0}.dll", "CsParser")),
                ProjectStructure = _null

            }.ActLike<IPipelineStep>();

            host.PipelineContainer.RegisterInstance<IPipelineStep>(step.Name, step);

            step = new
            {
                Name = "XmlSchemasStep",
                StepAssemblyFile = Path.Combine(path, string.Format(@"CodeGenerationPipeline\PipelineGenerators\{0}\bin\Debug\{0}.dll", "XmlSchemas")),
                ProjectStructure = _null

            }.ActLike<IPipelineStep>();

            host.PipelineContainer.RegisterInstance<IPipelineStep>(step.Name, step);
        }

        public XmlSchema CodeGeneratorInterfaceSchema
        {
            get
            {
                if (codeGeneratorInterfaceSchema == null)
                {
                    var resourceStream = Assembly.GetAssembly(this.GetType()).GetManifestResourceStream(@"CodeGenerationPipeline.CodeGeneratorInterface.xsd");

                    codeGeneratorInterfaceSchema = XmlSchema.Read(resourceStream, OnValidationEvent);
                }

                return codeGeneratorInterfaceSchema;
            }
        }

        private void OnValidationEvent(object sender, ValidationEventArgs e)
        {
            Debugger.Break();
        }

        public void GenerateMiddleLayer(string id)
        {
            var buildInfo = new BuildInfo(null);

            var daemon = new DataBindingsDaemon(host);

            host.ClearEventQueue();
            host.PostEventShowUI("DataBindings.dll", id, daemon, new TimeSpan(0, 15, 0));

            if (daemon.ResponseValue == BuildUIResponse.Succeeded)
            {
                if (daemon.BindingsTrees.Count > 0)
                {
                    this.GenerateFrom(daemon.BindingsTrees);
                }
            }
        }

        public void GenerateFrom(List<IBindingsTree> bindings)
        {
            var buildFilters = host.BuildFilters.Values.ToList<IBuildFilter>();

            try
            {
                var package = (ICodeGenerationPackage)null;
                var directories = new List<DirectoryInfo>();
                var first = true;
                var services = bindings.Select(b => b.AbstraXProviderService).Distinct();
                var abortGeneration = false;

                this.OnOutput += (e, args) =>
                {
                    buildFilters.ForEach(f => f.OnOutput(e, args));
                };

                buildFilters.ForEach(f => f.OnGenerationStarted());

                HandleFilters(bindings, buildFilters, out abortGeneration);

                if (abortGeneration)
                {
                    return;
                }

                foreach (var service in services)
                {
                    var serviceBindings = bindings.Where(b => b.AbstraXProviderService == service).ToList<IBindingsTree>();

                    if (serviceBindings.Count > 0)
                    {
                        var pipelineSteps = new List<IPipelineStep>();

                        // ********* hard-coded steps for testing

                        var projectRoot = new
                        {
                            FindLayer = Return<IProjectStructure>.Arguments<string>((s) => (IProjectStructure)host.Solutions[0].Projects.Where(p => p.Name == "SolutionFramework.Web" && p.ProjectType == "KnownToBeMSBuildFormat").Single())
                        }
                        .ActLike<IProjectRoot>();

                        if (service.InitialPipelineSteps != null)
                        {
                            pipelineSteps.AddRange(service.InitialPipelineSteps.Values);
                        }

                        //pipelineSteps.Add(new
                        //{
                        //    StepAssemblyFile = Path.Combine(path, @"PipelineGenerators\DomainServices\bin\Debug\Dmb.0omainServices.dll"),
                        //    ProjectStructure = host.Solutions[0].Projects.Where(p => p.Name == "SolutionFramework.Service" && p.ProjectType == "KnownToBeMSBuildFormat").Single().Items.Where(i => i.Name == "CodeGenerationPipelineDomainService.cs").Single(i => { i.ProjectRoot = projectRoot; return true; })

                        //}.ActLike<IPipelineStep>());

                        //pipelineSteps.Add(new
                        //{
                        //    StepAssemblyFile = Path.Combine(path, @"PipelineGenerators\PresentationServices\bin\Debug\PresentationServices.dll"),
                        //    ProjectStructure = host.Solutions[0].Projects.Where(p => p.Name == "CodeGenerationPipeline" && p.ProjectType == "KnownToBeMSBuildFormat").Single().Items.Where(i => i.Name.Replace(@"\", "") == "Services").Single(i => { i.ProjectRoot = projectRoot; return true; })

                        //}.ActLike<IPipelineStep>());

                        // ********* end testing

                        var bindingsBuilder = (IBindingsBuilder)new BindingsBuilder();

                        buildFilters.ForEach(f => f.OnStepsAdded(pipelineSteps));
                        buildFilters.ForEach(f => f.OnBindingsBuild(serviceBindings, ref bindingsBuilder));

                        elementBuilds = bindingsBuilder.GenerateBuilds(serviceBindings);

                        var rootCount = elementBuilds.Where(e => e.Value.IsRootObject).Count();
                        var workspace = host.WorkspacePath + DateTime.Now.ToString("yyyyMMdd_HHmmss_fffffff");

                        foreach (var pipelineStep in pipelineSteps)
                        {
                            var assemblyFile = pipelineStep.StepAssemblyFile;
                            var generatorAssembly = Assembly.LoadFrom(assemblyFile);
                            var generatorType = generatorAssembly.GetTypes().Where(t => t.GetInterfaces().Any(i => i.Name == "IGenerator")).Single();
                            var generator = (IGenerator)Activator.CreateInstance(generatorType);
                            var workspacePath = Path.Combine(workspace, generator.Name.Replace(" ", "") + "_" + Guid.NewGuid().ToString().Replace("-", ""));
                            var directoryInfo = new DirectoryInfo(workspacePath);
                            var skipStep = false;
                            var templateEngineHost = new PipelineTemplateEngineHost();

                            generator.PipelineStep = pipelineStep;

                            if (directoryInfo.Exists)
                            {
                                directoryInfo.GetFiles().ToList().ForEach(f => f.Delete());
                            }
                            else
                            {
                                directoryInfo.Create();
                            }

                            directories.Add(directoryInfo);

                            if (Environment.Is64BitOperatingSystem)
                            {
                                generator.SDKFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), @"Microsoft SDKs\Windows\v7.0A\Bin\");
                                generator.DotNetFxFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), @"Microsoft.NET\Framework64\v4.0.30319\");
                            }
                            else
                            {
                                generator.SDKFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Microsoft SDKs\Windows\v7.0A\Bin\");
                                generator.DotNetFxFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), @"Microsoft.NET\Framework\v4.0.30319\");
                            }

                            templateEngineHost.OnGeneratorFileWithBuildPreBuild += new OnGeneratorFileWithBuildPreBuildHandler(OnGeneratorFileWithBuildPreBuild);
                            templateEngineHost.OnGeneratorFileWithBuildPostBuild += new OnGeneratorFileWithBuildPostBuildHandler(OnGeneratorFileWithBuildPostBuild);
                            templateEngineHost.OnGeneratorFileWithListPreBuild += new OnGeneratorFileWithListPreBuildHandler(OnGeneratorFileWithListPreBuild);
                            templateEngineHost.OnGeneratorFileWithListPostBuild += new OnGeneratorFileWithListPostBuildHandler(OnGeneratorFileWithListPostBuild);
                            templateEngineHost.OnGeneratorFileWithListCodeWriter += new OnGeneratorFileWithListCodeWriterHandler(OnGeneratorFileWithListCodeWriter);
                            templateEngineHost.OnGeneratorFileWithBuildCodeWriter += new OnGeneratorFileWithBuildCodeWriterHandler(OnGeneratorFileWithBuildCodeWriter);

                            generator.TemplateEngineHost = templateEngineHost;
                            generator.OnWatchProcess += new WatchProcess(OnWatchProcess);
                            generator.DomainHostApplication = host;
                            generator.EventsService = this.EventsService;
                            generator.OnFileGenerated += (project, subPath, file) =>
                            {
                                var fileName = Path.Combine(new FileInfo(project.FileName).DirectoryName, subPath, file.Name);

                                buildFilters.ForEach(f => f.OnFileGenerated(fileName));
                            };

                            currentStep = pipelineStep;
                            currentGenerator = generator;

                            if (first)
                            {
                                HandleFilters(buildFilters, pipelineStep, directoryInfo, out skipStep, out abortGeneration);

                                if (abortGeneration)
                                {
                                    return;
                                }

                                if (skipStep)
                                {
                                    continue;
                                }

                                var package2 = (ICodeGenerationPackage)null;

                                package2 = generator.GenerateFrom(elementBuilds, new DirectoryInfo(workspacePath));

                                if (generator.LastException is StepAbortionException)
                                {
                                    buildFilters.ForEach(f => f.OnBuildGeneralError(generator.LastException));
                                    continue;
                                }
                                else if (generator.LastException != null)
                                {
                                    buildFilters.ForEach(f => f.OnBuildGeneralError(generator.LastException));
                                }

                                if (package2 == null)
                                {
                                    var stackTrace = new StackTrace(true);

                                    var exception = new Exception(string.Format("Fatal error. Generator '{0}' from step '{1}' returned an unexpected null package", generator.Name, pipelineStep.Name));

                                    buildFilters.ForEach(f => f.OnBuildFatalError(exception, stackTrace.ToString()));
                                    return;
                                }

                                first = false;
                                package = package2;

                                buildFilters.ForEach(f => f.OnStepPostGenerate(pipelineStep, package));
                            }
                            else
                            {
                                HandleFilters(buildFilters, pipelineStep, package, out skipStep, out abortGeneration);

                                if (abortGeneration)
                                {
                                    return;
                                }

                                if (skipStep)
                                {
                                    continue;
                                }

                                package = generator.GenerateFromPackage(package);

                                buildFilters.ForEach(f => f.OnStepPostGenerate(pipelineStep, package));
                            }
                        }
                    }
                }

                buildFilters.ForEach(f => f.OnGenerationComplete());
            }
            catch (Exception ex)
            {
                buildFilters.ForEach(f => f.OnBuildFatalError(ex));
            }
        }

        private void OnGeneratorFileWithListPreBuild(ref Type generatorType, ref FileInfo fileInfo, ref string _namespace, ref IEnumerable list, out bool skipBuildFile, out bool abortStep)
        {
            var abortStep2 = false;
            var skipBuildFile2 = false;
            var fileInfo2 = fileInfo;
            var _namespace2 = _namespace;
            var list2 = list;
            var generatorType2 = generatorType;
            var buildFilters = host.BuildFilters.Values.ToList<IBuildFilter>();

            skipBuildFile = false;
            abortStep = false;

            buildFilters.ForEach(f =>
            {
                if (!abortStep2)
                {
                    f.OnGeneratorFileWithListPreBuild(currentGenerator, ref generatorType2, ref fileInfo2, ref _namespace2, ref list2, out skipBuildFile2, out abortStep2);

                    if (abortStep2)
                    {
                        buildFilters.ForEach(f2 =>
                        {
                            if (f != f2)
                            {
                                f2.OnStepAborted(currentGenerator, currentStep);
                            }
                        });
                    }

                    if (skipBuildFile2)
                    {
                        buildFilters.ForEach(f2 =>
                        {
                            if (f != f2)
                            {
                                f2.OnBuildFileSkipped(currentGenerator, currentStep, fileInfo2);
                            }
                        });
                    }
                }
            });

            abortStep = abortStep2;
            skipBuildFile = skipBuildFile2;
            fileInfo = fileInfo2;
            _namespace = _namespace2;
            list = list2;
            generatorType = generatorType2;
        }

        private void OnGeneratorFileWithBuildCodeWriter(IElementBuild build, FileInfo fileInfo, string _namespace, Dictionary<string, object> extraValues, ref ICodeWriter codeWriter)
        {
            var buildFilters = host.BuildFilters.Values.ToList<IBuildFilter>();
            var codeWriter2 = codeWriter;

            buildFilters.ForEach(f =>
            {
                f.OnGeneratorFileWithBuildCodeWriter(currentGenerator, build, fileInfo, _namespace, extraValues, ref codeWriter2);
            });
        }

        private void OnGeneratorFileWithListCodeWriter(FileInfo fileInfo, string _namespace, IEnumerable list, ref ICodeWriter codeWriter)
        {
            var buildFilters = host.BuildFilters.Values.ToList<IBuildFilter>();
            var codeWriter2 = codeWriter;

            buildFilters.ForEach(f =>
            {
                f.OnGeneratorFileWithListCodeWriter(currentGenerator, fileInfo, _namespace, list, ref codeWriter2);
            });
        }

        private void OnGeneratorFileWithBuildPreBuild(ref Type generatorType, ref IElementBuild build, ref FileInfo fileInfo, ref string _namespace, ref Dictionary<string, object> extraValues, out bool skipBuildFile, out bool abortStep)
        {
            var abortStep2 = false;
            var skipBuildFile2 = false;
            var build2 = build;
            var fileInfo2 = fileInfo;
            var _namespace2 = _namespace;
            var extraValues2 = extraValues;
            var generatorType2 = generatorType;
            var buildFilters = host.BuildFilters.Values.ToList<IBuildFilter>();

            skipBuildFile = false;
            abortStep = false;

            buildFilters.ForEach(f =>
            {
                if (!abortStep2)
                {
                    f.OnGeneratorFileWithBuildPreBuild(currentGenerator, ref generatorType2, ref build2, ref fileInfo2, ref _namespace2, ref extraValues2, out skipBuildFile2, out abortStep2);

                    if (abortStep2)
                    {
                        buildFilters.ForEach(f2 =>
                        {
                            if (f != f2)
                            {
                                f2.OnStepAborted(currentGenerator, currentStep);
                            }
                        });
                    }

                    if (skipBuildFile2)
                    {
                        buildFilters.ForEach(f2 =>
                        {
                            if (f != f2)
                            {
                                f2.OnBuildFileSkipped(currentGenerator, currentStep, fileInfo2);
                            }
                        });
                    }
                }
            });

            abortStep = abortStep2;
            skipBuildFile = skipBuildFile2;
            fileInfo = fileInfo2;
            _namespace = _namespace2;
            extraValues = extraValues2;
            generatorType = generatorType2;
        }

        private void OnGeneratorFileWithListPostBuild(ref FileInfo fileInfo, string _namespace, IEnumerable list, ICodeParser codeParser, ref string output)
        {
            var fileInfo2 = fileInfo;
            var output2 = output;
            var buildFilters = host.BuildFilters.Values.ToList<IBuildFilter>();

            buildFilters.ForEach(f =>
            {
                f.OnGeneratorFileWithListPostBuild(currentGenerator, ref fileInfo2, _namespace, list, codeParser, ref output2);
            });

            fileInfo = fileInfo2;
            output = output2;
        }

        private void OnGeneratorFileWithBuildPostBuild(IElementBuild build, ref FileInfo fileInfo, string _namespace, Dictionary<string, object> extraValues, ICodeParser codeParser, ref string output)
        {
            var fileInfo2 = fileInfo;
            var output2 = output;
            var buildFilters = host.BuildFilters.Values.ToList<IBuildFilter>();

            buildFilters.ForEach(f =>
            {
                f.OnGeneratorFileWithBuildPostBuild(currentGenerator, build, ref fileInfo2, _namespace, extraValues, codeParser, ref output2);
            });

            fileInfo = fileInfo2;
            output = output2;
        }

        private void HandleFilters(List<IBuildFilter> buildFilters, IPipelineStep pipelineStep, ICodeGenerationPackage incomingPackage, out bool skipStep, out bool abortGeneration)
        {
            var abortGeneration2 = false;
            var skipStep2 = false;

            abortGeneration = false;
            skipStep = false;

            buildFilters.ForEach(f =>
            {
                if (!abortGeneration2)
                {
                    f.OnStepPreGenerate(pipelineStep, incomingPackage, out skipStep2, out abortGeneration2);

                    if (abortGeneration2)
                    {
                        buildFilters.ForEach(f2 =>
                        {
                            if (f != f2)
                            {
                                f2.OnGenerationAborted();
                            }
                        });
                    }

                    if (skipStep2)
                    {
                        buildFilters.ForEach(f2 =>
                        {
                            if (f != f2)
                            {
                                f2.OnStepSkipped(pipelineStep);
                            }
                        });
                    }
                }
            });

            abortGeneration = abortGeneration2;
            skipStep = skipStep2;
        }

        private void HandleFilters(List<IBuildFilter> buildFilters, IPipelineStep pipelineStep, DirectoryInfo directoryInfo, out bool skipStep, out bool abortGeneration)
        {
            var abortGeneration2 = false;
            var skipStep2 = false;

            abortGeneration = false;
            skipStep = false;

            buildFilters.ForEach(f =>
            {
                if (!abortGeneration2)
                {
                    f.OnStepPreGenerate(pipelineStep, elementBuilds, directoryInfo, out skipStep2, out abortGeneration2);

                    if (abortGeneration2)
                    {
                        buildFilters.ForEach(f2 =>
                        {
                            if (f != f2)
                            {
                                f2.OnGenerationAborted();
                            }
                        });
                    }

                    if (skipStep2)
                    {
                        buildFilters.ForEach(f2 =>
                        {
                            if (f != f2)
                            {
                                f2.OnStepSkipped(pipelineStep);
                            }
                        });
                    }
                }
            });

            abortGeneration = abortGeneration2;
            skipStep = skipStep2;
        }

        private void HandleFilters(List<IBindingsTree> bindings, List<IBuildFilter> buildFilters, out bool abortGeneration)
        {
            var abortGeneration2 = false;

            abortGeneration = false;

            buildFilters.ForEach(f =>
            {
                if (!abortGeneration2)
                {
                    f.OnGenerateFrom(bindings, out abortGeneration2);

                    if (abortGeneration2)
                    {
                        buildFilters.ForEach(f2 =>
                        {
                            if (f != f2)
                            {
                                f2.OnGenerationAborted();
                            }
                        });
                    }
                }
            });

            abortGeneration = abortGeneration2;
        }

        private void OnWatchProcess(IGenerator generator, Process process, bool blockUntilFinished, int timeout)
        {
            Thread watchThread = null;

            try
            {
                var exit = false;
                var startTick = Environment.TickCount;
                BuildMessageEventArgs eventArgs;

                var processWatchInfo = new ProcessWatchInfo
                {
                    Generator = generator,
                    Process = process,
                    ProcessExitEvent = new ManualResetEvent(false)
                };

                watchThread = new Thread(o =>
                {
                    var watchInfo = (ProcessWatchInfo)o;

                    while (!exit)
                    {
                        Thread.Sleep(100);

                        if (Environment.TickCount - startTick > timeout)
                        {
                            exit = true;

                            watchInfo.Generator.OnProcessTimeout();
                            processWatchInfo.ProcessExitEvent.Set();

                            break;
                        }
                    }
                });

                process.OutputDataReceived += (s, e) =>
                {
                    var output = e.Data;

                    if (!string.IsNullOrEmpty(output))
                    {
                        eventArgs = new BuildMessageEventArgs(output, process.ProcessName, process.ProcessName, MessageImportance.Normal);

                        OnOutput(this, eventArgs);
                        AppendOutput(generator, output);

                        generator.OnOutput(output);
                    }
                };

                process.ErrorDataReceived += (s, e) =>
                {
                    var error = e.Data;

                    if (!string.IsNullOrEmpty(error))
                    {
                        eventArgs = new BuildMessageEventArgs(error, process.ProcessName, process.ProcessName, MessageImportance.High);

                        OnOutput(this, eventArgs);
                        AppendOutput(generator, error);

                        generator.OnError(0, null, error);
                    }
                };

                process.EnableRaisingEvents = true;
                process.StartInfo.RedirectStandardError = true;
                process.StartInfo.RedirectStandardOutput = true;
                process.StartInfo.UseShellExecute = false;

                process.Exited += (s, e) =>
                {
                    Thread.Sleep(1000);

                    eventArgs = new BuildMessageEventArgs("\r\nProcess exited", process.ProcessName, process.ProcessName, MessageImportance.Normal);

                    OnOutput(this, eventArgs);
                    AppendOutput(generator, "\r\nProcess exited");

                    exit = true;

                    generator.OnProcessExited(process.ExitCode);

                    processWatchInfo.ProcessExitEvent.Set();
                };

                var fileSystemWatcher = new FileSystemWatcher(generator.WorkspaceDirectory);

                fileSystemWatcher.EnableRaisingEvents = true;
                fileSystemWatcher.IncludeSubdirectories = true;

                fileSystemWatcher.Created += (s, e) =>
                {
                    if (e.Name != "Output.log")
                    {
                        generator.OnFileCreated(new FileInfo(e.FullPath));
                    }
                };

                fileSystemWatcher.Changed += (s, e) =>
                {
                    if (e.Name != "Output.log")
                    {
                        generator.OnFileChanged(new FileInfo(e.FullPath));
                    }
                };

                fileSystemWatcher.Renamed += (s, e) =>
                {
                    if (e.Name != "Output.log")
                    {
                        generator.OnFileRenamed(new FileInfo(e.FullPath), new FileInfo(e.OldFullPath));
                    }
                };

                fileSystemWatcher.Deleted += (s, e) =>
                {
                    generator.OnFileDeleted(new FileInfo(e.FullPath));
                };

                fileSystemWatcher.Error += (s, e) =>
                {
                    generator.OnError(0, e.GetException(), "File System Watcher error.");
                };

                eventArgs = new BuildMessageEventArgs(process.StartInfo.FileName + " " + process.StartInfo.Arguments + "\r\n", process.ProcessName, process.ProcessName, MessageImportance.Normal);

                OnOutput(this, eventArgs);
                AppendOutput(generator, process.StartInfo.FileName + " " + process.StartInfo.Arguments + "\r\n");

                watchThread.Start(processWatchInfo);
                process.Start();

                process.BeginErrorReadLine();
                process.BeginOutputReadLine();

                watchThread.Join();

                processWatchInfo.ProcessExitEvent.WaitOne();
            }
            catch (Exception ex)
            {
                if (watchThread != null && watchThread.IsAlive)
                {
                    watchThread.Abort();
                }

                generator.OnError(0, ex, "OnWatchProcess error.");
            }
        }

        private void AppendOutput(IGenerator generator, string output)
        {
            var writer = new StreamWriter(Path.Combine(generator.WorkspaceDirectory, "Output.log"), true);

            writer.WriteLine(output);

            writer.Close();
        }
    }
}
